/*
 * 版权所有 (C) 2015 知启蒙(ZHIQIM) 保留所有权利。[遇见知启蒙，邂逅框架梦]
 * 
 * https://zhiqim.org/project/zhiqim_framework/zhiqim_httpd.htm
 *
 * Zhiqim Httpd is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *          http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.zhiqim.httpd.context.service;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;

import org.zhiqim.httpd.HttpContext;
import org.zhiqim.httpd.HttpExecutor;
import org.zhiqim.httpd.HttpRequest;
import org.zhiqim.httpd.HttpResponse;
import org.zhiqim.httpd.context.ZmlContextConstants;
import org.zhiqim.httpd.context.core.Context;
import org.zhiqim.httpd.util.Responses;
import org.zhiqim.kernel.annotation.AnFilterNot;
import org.zhiqim.kernel.httpclient.HttpGetStream;
import org.zhiqim.kernel.logging.Log;
import org.zhiqim.kernel.logging.LogFactory;
import org.zhiqim.kernel.model.codes.Base64;
import org.zhiqim.kernel.model.codes.URI;
import org.zhiqim.kernel.util.Files;
import org.zhiqim.kernel.util.Ids;
import org.zhiqim.kernel.util.Streams;
import org.zhiqim.kernel.util.Strings;
import org.zhiqim.kernel.util.Urls;
import org.zhiqim.kernel.util.Validates;

/**
 * 上传服务，用于固定访问地址为/service/upload，实际处理在forward中
 *
 * @version v1.0.0 @author zouzhigang 2014-3-21 新建与整理
 */
public class UploadService implements HttpExecutor, ZmlContextConstants
{
    private static final Log log = LogFactory.getLog(UploadService.class);
    private static final String[] FILE_DIR_NOT_FORMAT = {".", "\\", "?", ":", "*", "\'", "\"", "<", ">", "|"};
    
    private HttpContext context;
    private String uploadRootDir;
    
    public UploadService(HttpContext context)
    {
        this.context = context;
    }
    
    @Override
    public boolean isMatch(String pathInContext)
    {
        return _PATH_SERVICE_UPLOAD_.equals(pathInContext);
    }
    
    @Override
    public void handle(HttpRequest request, HttpResponse response) throws IOException
    {
        String sessionName = Strings.toString(request.getSessionName(), "guest");
        
        /**********************************************************************/
        //第一步，文件根目录配置检查，目前只作一次检查，后续可能要支持新旧两个
        /**********************************************************************/
        
        String result = chkUploadRootDir();
        if (result != null)
        {//文件根目录检查失败
            log.error("[Upload][%s][检查上传根目录出错：%s]", sessionName, result, _SERV_UPLOAD_ROOT_DIR_);
            response.sendError(_403_FORBIDDEN_);
            return;
        }
        
        /**********************************************************************/
        //第二步，判断上传参数
        /**********************************************************************/
        int mode = request.getHeaderInt(_X_RESPONSED_MODE_, 0);
        String fileDir = request.getHeader(_X_UPLOAD_FILE_DIR_);
        String fileName = request.getHeader(_X_UPLOAD_FILE_NAME_);
        int contentLength = request.getContentLength();
        
        if (mode < 0 || mode > 1 || contentLength <= 0)
        {//响应模式0|1，内容不能为空
            log.error("[Upload][%s][参数不正确]", sessionName);
            response.sendError(_412_PRECONDITION_FAILED_);
            return;
        }
        
        fileDir = Urls.decodeUTF8(fileDir);
        fileName = Urls.decodeUTF8(fileName);
        
        if (Validates.isStrContainStrArr(fileDir, FILE_DIR_NOT_FORMAT) || Validates.isEmptyBlank(fileName))
        {//文件目录不支持.\:*?'"<>|，文件名不能为空
            log.error("[Upload][%s][参数不正确]", sessionName);
            response.sendError(_412_PRECONDITION_FAILED_);
            return;
        }
        
        /**********************************************************/
        //第三步，判断文件PATH和URL，生成并返回
        //1.对文件目录进行检查，并生成URL对应的BASE64编码的目录名
        //2.写入文件并返回URL
        /**********************************************************/
        
        //2.检查文件路径是否合规
        String[] results = chkFilePath(fileDir, fileName);
        if (results[0] == null)
        {
            log.error("[Upload][%s][%s][%s]", sessionName, results[1], request.getHeader(_X_UPLOAD_FILE_DIR_));
            response.sendError(_403_FORBIDDEN_);
            return;
        }
        
        String filePath = results[0], fileUrl = results[1];
        
        //3.保存文件
        byte[] data = Streams.getBytes(request.getInputStream(), contentLength);
        Files.writeReturnSize(filePath, data);
        
        //4.返回URL
        log.info("[Upload][%s][上传完成][%s]", sessionName, fileName);
        HashMap<String, String> headerMap = new HashMap<String, String>();
        headerMap.put(_X_UPLOAD_FILE_URL_, fileUrl);
        
        Responses.doReturnMessage(response, mode, headerMap);
    }
    
    /************************************************************************************************/
    //公开静态方法，提供给AJAX调用的本地上传（upload）&网络文件上传（uploadwww）
    /************************************************************************************************/
    
    @AnFilterNot /** 上传本地文件 */
    public static void upload(HttpRequest request) throws Exception
    {
        //1.从上传服务中检查文件路径
        UploadService upload = ((Context)request.getContext()).getServiceExecutor().getUploadService();
        String result = upload.chkUploadRootDir();
        if (result != null)
        {
            request.setResponseError(result);
            return;
        }
        
        //2.再检查文件路径是否有效
        String fileDir = request.getParameterNoFileterOnCNT("fileDir");
        String fileName = request.getParameterNoFileterOnCNT("fileName");
        
        String[] results = upload.chkFilePath(fileDir, fileName);
        if (results[0] == null)
        {
            request.setResponseError(results[1]);
            return;
        }
        
        //3.读出BASE64的文件数据，格式如，逗号后面的为内容
        String fileData = request.getParameterNoFileterOnCNT("fileData");
        String[] fileBase64 = fileData.split(",");
        if (fileBase64 == null || fileBase64.length != 2)
        {
            request.setResponseError("不是文件上传格式的BASE64内容");
            return;
        }
        
        //4.最后写入文件到到磁盘，响应值为文件URL
        byte[] bytes = Base64.decode(fileBase64[1]);
        Files.write(results[0], bytes);
        request.setResponseResult(results[1]);
    }
    
    @AnFilterNot /** 上传网络文件 */
    public static void uploadwww(HttpRequest request) throws Exception
    {
        //1.从上传服务中检查文件路径
        UploadService upload = ((Context)request.getContext()).getServiceExecutor().getUploadService();
        String result = upload.chkUploadRootDir();
        if (result != null)
        {
            request.setResponseError(result);
            return;
        }
        
        //2.先判断URL的有效性
        String fileDir = request.getParameterNoFileterOnCNT("fileDir");
        String fileUrl = request.getParameterNoFileterOnCNT("fileUrl");
        
        URI uri = new URI().parse(fileUrl);
        if (!uri.isParsed())
        {
            request.setResponseError("网络文件URL无效");
            return;
        }
        
        //3.再检查文件路径是否有效
        String fileName = Files.getFileNameLinux(uri.getPath());
        
        String[] results = upload.chkFilePath(fileDir, fileName);
        if (results[0] == null)
        {
            request.setResponseError(results[1]);
            return;
        }
        
        //4.然后下载文件，判断网络文件有效性，最多连5秒读阻塞10秒
        HttpGetStream conn = new HttpGetStream(fileUrl);
        conn.setConnectTimeout(5);
        conn.setReadTimeout(10);
        conn.execute();
        if (conn.getResponseStatus() != 200)
        {
            request.setResponseResult(701, "读取网络文件失败或超时");
            return;
        }
        
        //5.最后写入文件到到磁盘，响应值为文件URL
        Files.write(results[0], conn.getBytes());
        request.setResponseResult(results[1]);
    }
    
    /************************************************************************************************/
    //公开方法，其他类也可以参考upload/uploadwww方法上传
    /************************************************************************************************/
    
    /** 检查上传根目录配置 */
    public String chkUploadRootDir()
    {
        if (uploadRootDir != null)
            return null;
        
        String rootDir = context.getAttributeString(_SERV_UPLOAD_ROOT_DIR_);
        if (Validates.isEmptyBlank(rootDir) || !Files.mkDirectory(rootDir))
            return "根目录未配置或根目录不允许操作";
        
        try
        {
            uploadRootDir = Files.toLinuxPath(new File(rootDir).getCanonicalPath());
            uploadRootDir = Strings.addEndsWith(uploadRootDir, "/");
            return null;
        }
        catch (IOException e)
        {
            return "根目录转换绝对路径时出错：" + e.getMessage();
        }
    }
    
    /** 获取上传根目录，==null表示根目录未配置或不可访问，!=null表示有效 */
    public String getUploadRootDir()
    {
        return (chkUploadRootDir() == null)?uploadRootDir:null;
    }
    
    /** 检查文件目录和文件名，注意检查传入前是否要decode，如果是parameter不要，header则需要 */
    public String[] chkFilePath(String fileDir, String fileName)
    {
        fileDir = Files.toLinuxPath(fileDir);
        fileDir = Strings.trim(fileDir, "/", "/");
        
        String fileDirPath = null;
        if (Validates.isEmptyBlank(fileDir))
            fileDirPath = uploadRootDir;
        else
        {
            fileDirPath = uploadRootDir + fileDir + "/";
            if (!Files.mkDirectory(fileDirPath))
            {
                return new String[]{null, "上传的文件目录无法创建"};
            }
        }
        
        //然后对文件重命名（UUID+原后缀）
        fileName  = Ids.uuid() + Files.fixFileExt(Files.getFileExt(fileName));
        String filePath = fileDirPath + fileName;
        
        String fileUrl = null;
        if (Validates.isEmptyBlank(fileDir))
            fileUrl = _PATH_UPLOAD_PREFIX_ + fileName;
        else
        {
            String fileDirUrl = Base64.encodeFileUrlUTF8(fileDir);
            fileUrl = _PATH_UPLOAD_PREFIX_ + fileDirUrl + "/" + fileName;
        }
        
        return new String[]{filePath, fileUrl};
    }
}
